/*
    libverbose is a library for use in troubleshooting other software
    Copyright (C) 2008 Brock Noland

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
#include "config.h"

#include <stdio.h>
#include <errno.h>
#include <syslog.h>
#include <string.h>
#include <stdlib.h>
#include <regex.h>
#include <unistd.h>
#include <pthread.h>
#include <limits.h>

struct small_char_node
{
    char value[50];
    struct small_char_node *next;
};
struct int_node
{
    int value;
    struct int_node *next;
};
struct thread_fd_count
{
    int fd;
    int count;
    pthread_t tid;
    struct thread_fd_count *next;
};
typedef struct small_char_node ip_pattern_t;
typedef struct int_node uid_node_t;
typedef struct int_node fd_node_t;
typedef struct thread_fd_count thread_fd_count_t;

#define CONFIG_OPTION_CONFIG_FILE   "/usr/local/libverbose/etc/libverbose.cfg"
#define CONFIG_OPTION_NUM_SENDS     10

#define CONFIG_OPTION_OUTGOING      "outgoing"
#define CONFIG_OPTION_POPEN     "popen"
#define CONFIG_OPTION_LOG       "log"
#define CONFIG_OPTION_IGNORE        "ignore"
#define CONFIG_OPTION_IP        "ip"
#define CONFIG_OPTION_UID       "uid"
#define CONFIG_OPTION_VERBOSE       "verbose"
#define CONFIG_OPTION_EXECL     "execl"
#define CONFIG_OPTION_EXECV     "execv"
#define CONFIG_OPTION_EXECLE        "execle"
#define CONFIG_OPTION_EXECVE        "execve"
#define CONFIG_OPTION_EXECLP        "execlp"
#define CONFIG_OPTION_EXECVP        "execvp"
#define SPACE_CHARACTERS        " \t\r\n"

/* list configuration options
 **/
static ip_pattern_t *ignored_ip_list = NULL;
static uid_node_t *logged_uid_list = NULL;
static uid_node_t *ignored_uid_list = NULL;
static thread_fd_count_t *logged_fd_list = NULL;
static pthread_rwlock_t logged_fd_lock;

/* booleans and counter configuration options
 **/
int configurable_log_outgoing = 0;
int configurable_log_popen = 0;
int configurable_num_sends_logged = CONFIG_OPTION_NUM_SENDS;
int configurable_log_write = 0;
int configurable_verbose = 0;
int configurable_log_execl = 0;
int configurable_log_execv = 0;
int configurable_log_execle = 0;
int configurable_log_execve = 0;
int configurable_log_execlp = 0;
int configurable_log_execvp = 0;

static void
log_everything (void)
{
    configurable_log_outgoing = 1;
    configurable_log_popen = 1;
    configurable_log_write = 1;
    configurable_verbose = 1;
    configurable_log_execl = 1;
    configurable_log_execv = 1;
    configurable_log_execle = 1;
    configurable_log_execve = 1;
    configurable_log_execlp = 1;
    configurable_log_execvp = 1;
}
static void
local_init (void)
{
    pthread_rwlock_init (&logged_fd_lock, NULL);
}
extern void
parse_config (void)
{
    local_init ();

    char line[LINE_MAX];
    FILE *fd = fopen (CONFIG_OPTION_CONFIG_FILE, "r");
    if (!fd)
      {
          syslog (LOG_INFO, "%s: could not open %s. Logging everything.",
                  LOG_PREFIX, CONFIG_OPTION_CONFIG_FILE);
          log_everything ();
          return;
      }
    while (fgets (line, LINE_MAX, fd) != NULL)
      {
          int i = 0;
          for (; i < strlen (line); i++)
            {
                if (line[i] == '#')
                  {
                      line[i] = '\0';   // truncate all comments
                      break;
                  }
            }
          if (strlen (line) == 0)
            {
                continue;
            }
          char *tok = strtok (line, SPACE_CHARACTERS);
          if (!tok)
            {
                continue;
            }
          if (!strcmp (tok, CONFIG_OPTION_LOG)) // log
            {
                tok = strtok (NULL, SPACE_CHARACTERS);
                if (!tok)
                  {
                      syslog (LOG_INFO,
                              "%s: incomplete configuration option %s",
                              LOG_PREFIX, CONFIG_OPTION_LOG);
                      continue;
                  }
                if (!strcmp (tok, CONFIG_OPTION_VERBOSE))       // verbose
                  {
                      configurable_verbose = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_OUTGOING)) // outgoing [ [0-9]* ]
                  {
                      configurable_log_outgoing = 1;
                      tok = strtok (NULL, SPACE_CHARACTERS);
                      if (tok)
                        {
                            configurable_num_sends_logged =
                                (int) strtol (tok, (char **) NULL, 10);
                            if (configurable_num_sends_logged == 0 && errno == ERANGE)
                              {
                                  syslog (LOG_INFO,
                                          "%s: value specified after %s is invalid",
                                          LOG_PREFIX, CONFIG_OPTION_OUTGOING);
                                  configurable_num_sends_logged =
                                      CONFIG_OPTION_NUM_SENDS;
                                  continue;
                              }
                        }
                  }
                else if (!strcmp (tok, CONFIG_OPTION_POPEN))    // popen()
                  {
                      configurable_log_popen = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_EXECL))    // exec*
                  {
                      configurable_log_execl = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_EXECV))
                  {
                      configurable_log_execv = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_EXECLE))
                  {
                      configurable_log_execle = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_EXECVE))
                  {
                      configurable_log_execve = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_EXECLP))
                  {
                      configurable_log_execlp = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_EXECVP))
                  {
                      configurable_log_execvp = 1;
                  }
                else if (!strcmp (tok, CONFIG_OPTION_UID))      // uid list
                  {
                      tok = strtok (NULL, SPACE_CHARACTERS);
                      if (!tok)
                        {
                            syslog (LOG_INFO,
                                    "%s: incomplete configuration option %s",
                                    LOG_PREFIX, CONFIG_OPTION_IP);
                            continue;
                        }
                      while (tok)
                        {
                            int uid = (int) strtol (tok, (char **) NULL, 10);
                            if (uid == 0 && errno == ERANGE)
                              {
                                  syslog (LOG_INFO,
                                          "%s: value %s specified after %s is invalid",
                                          LOG_PREFIX, tok, CONFIG_OPTION_UID);
                              }
                            else
                              {
                                  uid_node_t *logged_uid =
                                      (uid_node_t *)
                                      malloc (sizeof (uid_node_t));
                                  logged_uid->value = uid;
                                  logged_uid->next = logged_uid_list;
                                  logged_uid_list = logged_uid;
                              }
                            tok = strtok (NULL, SPACE_CHARACTERS);
                        }
                  }
                else            // log [unknown configuration item]
                  {
                      syslog (LOG_INFO, "%s: unknown configuration option %s",
                              LOG_PREFIX, tok);
                      continue;
                  }
            }
          else if (!strcmp (tok, CONFIG_OPTION_IGNORE)) // ignore
            {
                tok = strtok (NULL, SPACE_CHARACTERS);
                if (!tok)
                  {
                      syslog (LOG_INFO,
                              "%s: incomplete configuration option %s",
                              LOG_PREFIX, CONFIG_OPTION_IGNORE);
                      continue;
                  }
                if (!strcmp (tok, CONFIG_OPTION_IP))    // ip list
                  {
                      tok = strtok (NULL, SPACE_CHARACTERS);
                      if (!tok)
                        {
                            syslog (LOG_INFO,
                                    "%s: incomplete configuration option %s",
                                    LOG_PREFIX, CONFIG_OPTION_IP);
                            continue;
                        }
                      while (tok)
                        {
                            ip_pattern_t *ignored_ip =
                                (ip_pattern_t *)
                                malloc (sizeof (ip_pattern_t));
                            strncpy (ignored_ip->value, tok,
                                     sizeof (ignored_ip->value));
                            ignored_ip->next = ignored_ip_list;
                            ignored_ip_list = ignored_ip;
                            tok = strtok (NULL, SPACE_CHARACTERS);
                        }
                  }
                else if (!strcmp (tok, CONFIG_OPTION_UID))      // uid list
                  {
                      tok = strtok (NULL, SPACE_CHARACTERS);
                      if (!tok)
                        {
                            syslog (LOG_INFO,
                                    "%s: incomplete configuration option %s",
                                    LOG_PREFIX, CONFIG_OPTION_UID);
                            continue;
                        }
                      while (tok)
                        {
                            int uid = (int) strtol (tok, (char **) NULL, 10);
                            if (uid == 0 && errno == ERANGE)
                              {
                                  syslog (LOG_INFO,
                                          "%s: value %s specified after %s is invalid",
                                          LOG_PREFIX, tok, CONFIG_OPTION_UID);
                              }
                            else
                              {
                                  uid_node_t *ignored_uid =
                                      (uid_node_t *)
                                      malloc (sizeof (uid_node_t));
                                  ignored_uid->value = uid;
                                  ignored_uid->next = ignored_uid_list;
                                  ignored_uid_list = ignored_uid;
                              }
                            tok = strtok (NULL, SPACE_CHARACTERS);
                        }
                  }
                else            // ignore [unknown configuration item]
                  {
                      syslog (LOG_INFO, "%s: unknown configuration option %s",
                              LOG_PREFIX, tok);
                      continue;
                  }
            }
          else                  // [unknown configuration item]
            {
                syslog (LOG_INFO, "%s: unknown configuration option %s",
                        LOG_PREFIX, tok);
                continue;
            }
      }
    fclose (fd);
}

/* Has this ip been specifically ignored */
extern int
logging_this_ip (const char *ip)
{
    if (!ip)
      {
          return 1;
      }
    ip_pattern_t *ignored_ip = ignored_ip_list;
    while (ignored_ip)
      {
          regex_t re;
          if (regcomp (&re, ignored_ip->value, REG_EXTENDED | REG_NOSUB))
            {
                /* some kind of error happend, include by default */
                regfree (&re);
                return 1;
            }
          if (!regexec (&re, ip, (size_t) 0, NULL, 0))
            {
                /* the ip matches the ignored pattern, exclude */
                regfree (&re);
                return 0;
            }
          ignored_ip = ignored_ip->next;
      }
    return 1;
}

/* Should we log this users actions */
extern int
logging_current_user (void)
{
    int uid = (int) geteuid ();
    /* If we have specifically asked for a uid list
     * to be logged we will only log those uid's. 
     **/
    if (logged_uid_list)
      {
          uid_node_t *logged_uid = logged_uid_list;
          while (logged_uid)
            {
                if (logged_uid->value == uid)
                  {
                      return 1;
                  }
                logged_uid = logged_uid->next;
            }
          return 0;
      }
    /* Otherwise we will log all users
     * who are not specifically ignored.
     **/
    uid_node_t *ignored_uid = ignored_uid_list;
    while (ignored_uid)
      {
          if (ignored_uid->value == uid)
            {
                return 0;
            }
          ignored_uid = ignored_uid->next;
      }
    /* We haven't ignored this user */
    return 1;
}
extern void
start_logging_fd (int fd)
{
    thread_fd_count_t *logged_fd =
        (thread_fd_count_t *) malloc (sizeof (thread_fd_count_t));
    logged_fd->fd = fd;
    logged_fd->count = 0;
    logged_fd->tid = pthread_self ();
    logged_fd->next = logged_fd_list;
    pthread_rwlock_wrlock (&logged_fd_lock);    // write lock

    logged_fd_list = logged_fd;

    pthread_rwlock_unlock (&logged_fd_lock);    // unlock

}
extern void
stop_logging_fd (int fd)
{
    pthread_t self = pthread_self ();
    pthread_rwlock_rdlock (&logged_fd_lock);    // read lock
    thread_fd_count_t *logged_fd = logged_fd_list;
    thread_fd_count_t *last_fd = NULL;
    while (logged_fd)
      {
          if (pthread_equal (logged_fd->tid, self) && logged_fd->fd == fd)
            {
                pthread_rwlock_unlock (&logged_fd_lock);        // unlock
                pthread_rwlock_wrlock (&logged_fd_lock);        // write lock
                if (last_fd)
                  {
                      // prev->next = current->next
                      last_fd->next = logged_fd->next;
                      free (logged_fd);
                  }
                else            // list is one item long
                  {
                      free (logged_fd);
                      logged_fd_list = NULL;
                  }
                break;
            }
          last_fd = logged_fd;
          logged_fd = logged_fd->next;
      }
    pthread_rwlock_unlock (&logged_fd_lock);    // unlock
}
extern int
logging_this_fd (int fd)
{
    int log = 0;
    pthread_t self = pthread_self ();
    pthread_rwlock_rdlock (&logged_fd_lock);    // read lock
    thread_fd_count_t *logged_fd = logged_fd_list;
    while (logged_fd)
      {
          if (pthread_equal (logged_fd->tid, self) && logged_fd->fd == fd)
            {
                pthread_rwlock_unlock (&logged_fd_lock);        // unlock
                pthread_rwlock_wrlock (&logged_fd_lock);        // write lock
                log = ++(logged_fd->count);
                break;
            }
          logged_fd = logged_fd->next;
      }
    pthread_rwlock_unlock (&logged_fd_lock);    // unlock
    return log;
}
